/*
 * Eurelis OpenCms Ant Task to create manifest.xml files for OpenCms modules. Copyright (C) Eurelis - http://www.eurelis.com
 * 
 * This program is free software: you can redistribute it and/or modify it under the terms version 3 of the GNU Lesser General Public
 * License as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the version 3 of the GNU Lesser
 * General Public License for more details.
 */
package com.eurelis.opencms.ant.task;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DefaultLogger;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.util.StringUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
import org.dom4j.tree.FlyweightCDATA;
import org.opencms.file.CmsPropertyDefinition;
import org.opencms.util.CmsFileUtil;
import org.opencms.util.CmsStringUtil;
import org.opencms.util.CmsUUID;

/**
 * ANT task that will create a manifest.xml file used to build an OpenCMS
 * module. The main objective is to facilitate team cooperation by integrating
 * module generation into the day to day development environment.
 * <p>
 * 
 * 
 * @author Vince Lambert
 * @author Achim Westermann
 * 
 */
public class ManifestBuilderTask extends Task {

	/**
	 * Models a dependency of the module.
	 * <p>
	 */
	public class Dependency {

		String m_dependencyName;

		String m_dependencyVersion;

		public Dependency() {

			super();
		}

		public String getName() {
			return this.m_dependencyName;
		}

		public String getVersion() {
			return this.m_dependencyVersion;
		}

		public void setName(final String dependencyName) {
			this.m_dependencyName = dependencyName;
		}

		public void setVersion(final String dependencyVersion) {
			this.m_dependencyVersion = dependencyVersion;
		}
	}

	/**
	 * Models an export point of the module.
	 * <p>
	 * 
	 */
	public class ExportPoint {

		String m_exportpointDst;

		String m_exportpointSrc;

		public ExportPoint() {
			super();
		}

		public String getDst() {
			return this.m_exportpointDst;
		}

		public String getSrc() {
			return this.m_exportpointSrc;
		}

		public void setDst(final String dst) {
			this.m_exportpointDst = dst;
		}

		public void setSrc(final String src) {
			this.m_exportpointSrc = src;
		}
	}

	/**
	 * Models a parameter of the module.
	 * <p>
	 */
	public class Parameter {

		String m_parameterName;

		String value;

		public Parameter() {
			super();
		}

		public String getName() {
			return this.m_parameterName;
		}

		public String getValue() {
			return this.value;
		}

		public void setName(final String parameterName) {
			this.m_parameterName = parameterName;
		}

		public void setValue(final String parameterValue) {
			this.value = parameterValue;
		}
	}

	/**
	 * Models a resource of the module.
	 * <p>
	 * 
	 */
	public class Resource {

		String m_resourceUri;

		public Resource() {
			super();
		}

		public String getUri() {
			return this.m_resourceUri;
		}

		public void setUri(final String uri) {
			this.m_resourceUri = uri;
		}
	}

	/**
	 * Represents a group of <code>{@link SiblingCandidate}</code>: files with
	 * the same resource uuid but different structure uuid and path.
	 * <p>
	 * 
	 * @author Achim Westermann
	 * 
	 */
	class SiblingGroup {

		/**
		 * Represents a sibling candidate.
		 * <p>
		 * 
		 * @author Achim Westermann
		 * 
		 */
		class SiblingCandidate {

			/** Path to the content file or null. */
			private String m_contentFilePath;

			/** The meta file path. */
			private String m_metaFilePath;

			/** The properties from the meta file. */
			private Properties m_propertyFile;

			/** The path in the virtual file system. */
			private String m_vfsPath;

			/**
			 * Creates a candidate based upon the given property file.
			 * <p>
			 * 
			 * This is here for performance reasons: The properties have to be
			 * parsed before this is invoked (for ordering in the correct
			 * sibling group) and should not be re-parsed.
			 * <p>
			 * 
			 * @param metaFile
			 *            contains the meta information.
			 * 
			 * @param VFSPath
			 *            the path in the virtual file system.
			 * 
			 * @param metaFileProps
			 *            the parsed properties of the meta file.
			 * 
			 * @throws IOException
			 *             if loading the meta file fails.
			 * 
			 * @throws FileNotFoundException
			 *             if the meta file does not exist.
			 */
			@SuppressWarnings("synthetic-access")
			SiblingCandidate(final Properties metaFileProps,
					final File metaFile, final String VFSPath)
					throws FileNotFoundException, IOException {
				this.m_metaFilePath = metaFile.getAbsolutePath();
				this.m_propertyFile = metaFileProps;
				String contentFilePath = ManifestBuilderTask.this
						.contentFilePathForPropertiesPath(metaFile
								.getAbsolutePath());
				File contentFile = new File(contentFilePath);
				if (contentFile.exists()) {
					this.m_contentFilePath = contentFile.getAbsolutePath()
							.replace(File.separatorChar, '/');
					ManifestBuilderTask.this.log("Content file "
							+ contentFile.getAbsolutePath() + " exist.",
							Project.MSG_DEBUG);
				} else {
					this.m_contentFilePath = null; // just to point out this
													// defined state!
					ManifestBuilderTask.this.log("Content file "
							+ contentFile.getAbsolutePath()
							+ " does not exist!", Project.MSG_DEBUG);
				}
				this.m_vfsPath = VFSPath;
				if (VFSPath.indexOf("__") > 0) {
					throw new BuildException(
							"Illegal VFS path: this is no content file but a meta file path!");
				}
			}

			/**
			 * Returns the path to the content file or null if this is a sibling
			 * or broken orphan entry.
			 * <p>
			 * 
			 * @return the path to the content file or null if this is a sibling
			 *         or broken orphan entry.
			 */
			protected String getContentFilePath() {
				return this.m_contentFilePath;
			}

			/**
			 * Returns the path to the meta file describing this sibling
			 * candidate.
			 * <p>
			 * 
			 * @return the path to the meta file describing this sibling
			 *         candidate.
			 */
			public String getMetaFilePath() {
				return this.m_metaFilePath;
			}

			/**
			 * Returns the properties of the underlying property file.
			 * <p>
			 * 
			 * @return the properties of the underlying property file.
			 */
			public Properties getPropertyFile() {
				return this.m_propertyFile;
			}

			/**
			 * Returns the resource uuid from the meta file of the candidate or
			 * <code>{@link CmsUUID#getNullUUID()}</code> if this was not
			 * specified in it.
			 * <p>
			 * 
			 * @return the resource uuid from the meta file of the candidate or
			 *         <code>{@link CmsUUID#getNullUUID()}</code> if this was
			 *         not specified in it.
			 */
			public CmsUUID getResourceUUID() {
				CmsUUID result = CmsUUID.getNullUUID();
				String resourceUUID = this.m_propertyFile
						.getProperty(OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_PREFIX
								+ OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_SUFFIX_RESOURCE_UUID);
				if (resourceUUID != null && resourceUUID.trim().length() > 0) {
					// throws NFE if broken value
					result = new CmsUUID(resourceUUID);
				}
				return result;
			}

			/**
			 * Returns the structure uuid from the meta file of the candidate or
			 * <code>{@link CmsUUID#getNullUUID()}</code> if this was not
			 * specified in it.
			 * <p>
			 * 
			 * @return the resource uuid from the meta file of the candidate or
			 *         <code>{@link CmsUUID#getNullUUID()}</code> if this was
			 *         not specified in it.
			 */
			public CmsUUID getStructureUUID() {
				CmsUUID result = CmsUUID.getNullUUID();
				String structureUUID = this.m_propertyFile
						.getProperty(OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_PREFIX
								+ OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_SUFFIX_STRUCTURE_UUID);
				if (structureUUID != null && structureUUID.trim().length() > 0) {
					// throws NFE if broken value
					result = new CmsUUID(structureUUID);
				}
				return result;
			}

			/**
			 * Returns the type from the meta file of the candidate or
			 * <code>null</code> if the type is not configured.
			 * <p>
			 * 
			 * @return the type from the meta file of the candidate or
			 *         <code>null</code> if the type is not configured.
			 */
			public String getType() {
				String type = this.m_propertyFile
						.getProperty(OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_PREFIX
								+ OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_SUFFIX_TYPE);
				if (CmsStringUtil.isEmptyOrWhitespaceOnly(type)) {
					if (ManifestBuilderTask.this.isGuessFileTypes()) {

						ManifestBuilderTask.this
								.log(
										"Guessing file type for "
												+ this.m_vfsPath
												+ " as is is not written in file "
												+ this.m_metaFilePath
												+ " as property "
												+ OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_PREFIX
												+ OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_SUFFIX_TYPE,
										Project.MSG_WARN);
						type = ManifestBuilderTask.this
								.guessFileType(this.m_vfsPath);
						// Don't guess again (validation and creation of
						// manifest.xml calls this):
						this.m_propertyFile
								.put(
										OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_PREFIX
												+ OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_SUFFIX_TYPE,
										type);
					}
				}
				return type;
			}

			/**
			 * Returns the path in the virtual file system.
			 * <p>
			 * 
			 * @return the path in the virtual file system.
			 */
			public String getVfsPath() {
				return this.m_vfsPath;
			}

			/**
			 * Returns true if a content file is associated with this sibling.
			 * <p>
			 * This is a flag showing that this sibling has a content file
			 * associated with it (thus makes all other siblings with the same
			 * UUID valid)
			 * <p>
			 * 
			 * @return true if a content file is associated with this sibling.
			 */
			public boolean isContentFile() {
				return this.m_contentFilePath != null;
			}

			/**
			 * Returns true if at least another candidate shares the same
			 * content file with this candidate (two siblings exist).
			 * <p>
			 * 
			 * This is only valid, after all sibling candidates have been added
			 * to the group.
			 * <p>
			 * 
			 * @return true if at least another candidate shares the same
			 *         content file with this candidate (two siblings exist).
			 */
			public boolean isSibling() {
				return SiblingGroup.this.getCandidateCount() > 1;
			}

			/**
			 * Returns the vfs path.
			 * <p>
			 * 
			 * @return the vfs path.
			 */
			@Override
			public String toString() {
				return this.m_vfsPath;
			}

			/**
			 * Validates this sibling candidate.
			 * <ul>
			 * <li>The type has to be configured in the meta file.</li>
			 * </ul>
			 * 
			 * <p>
			 * The actions taken are controlled by the ANT task attribute
			 * 
			 * <code>{@link ManifestBuilderTask#setFailOnIllegalMetaFile(boolean)}</code>
			 * and
			 * <code>{@link ManifestBuilderTask#setDeleteIllegalMetaFiles(boolean)}</code>
			 * . If those are set to false and true various actions may be
			 * performed to "heal" a broken build environment:
			 * <ul>
			 * <li>Delete illegal meta files (missing type, missing content file
			 * in sibling group -> remainders from content file deletion with
			 * forgetting to delete meta file)</li>
			 * </ul>
			 * <p>
			 * 
			 * @return true if everything is OK, false if errors were found but
			 *         actions (e.g. fixing the meta file by guessing the type
			 *         by suffix) have been taken to wipe them out (in this case
			 *         this group will be empty afterwards and may be dropped)
			 *         or throws a build exceptions if errors were found and no
			 *         actions have been taken.
			 */
			public void validate() {

				String type = this.getType();
				if (type == null || type.trim().length() == 0) {
					throw new BuildException(
							"Missing type ("
									+ OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_PREFIX
									+ OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_SUFFIX_TYPE
									+ ") missing in meta file: "
									+ this.getMetaFilePath()
									+ ".\nYou could also try to turn on the automatic detection via attribute \"guessFileTypes\".");
				}
			}
		}

		private List<SiblingCandidate> m_members = new LinkedList<SiblingCandidate>();

		/**
		 * Adds a new sibling candidate to this group.
		 * <p>
		 * 
		 * @param metaFile
		 *            the file with the information (property file).
		 * 
		 * @param metaFileProps
		 *            the properties parsed from the property file.
		 * 
		 * @param vfsPath
		 *            the path in the virtual file system.
		 * 
		 * @return the newly created sibling candidate.
		 * 
		 * @param folder
		 *            required for content file path lookup.
		 * 
		 * @throws FileNotFoundException
		 *             if something bad happens.
		 * 
		 * @throws IOException
		 *             if something bad happens.
		 */
		public SiblingCandidate addCandidate(final File metaFile,
				Properties metaFileProps, String vfsPath)
				throws FileNotFoundException, IOException {
			SiblingCandidate result = new SiblingCandidate(metaFileProps,
					metaFile, vfsPath);
			boolean added = this.m_members.add(result);
			if (!added) {
				throw new BuildException("Unable to add sibling "
						+ metaFile.getAbsolutePath() + " to sibling group: "
						+ this);
			} else {
				ManifestBuilderTask.this.log("Added " + metaFile
						+ " to sibling group: " + this, Project.MSG_VERBOSE);
			}
			return result;
		}

		/**
		 * Returns the amount of candidates in this sibling group.
		 * <p>
		 * 
		 * @return the amount of candidates in this sibling group.
		 */
		public int getCandidateCount() {
			return this.m_members.size();
		}

		/**
		 * Returns the <code>{@link SiblingCandidate}</code> that has the
		 * matching content entry in the eurelis ant build environment or null
		 * if this is an invalid sibling group.
		 * <p>
		 * 
		 * This must only be called after all property files in the build
		 * environment have been parsed and put into sibling groups.
		 * <p>
		 * 
		 * If this returns false the complete group is messed up and must not be
		 * processed into a manifest.xml for OpenCms VFS import (as an exception
		 * would arise when importing which might be overseen).
		 * <p>
		 * 
		 * @return the <code>{@link SiblingCandidate}</code> that has the
		 *         matching content entry in the eurelis ant build environment
		 *         or null if this is an invalid sibling group.
		 */
		public SiblingCandidate getCandidateWithContent() {
			SiblingCandidate result = null;
			for (SiblingCandidate candidate : this.m_members) {
				if (candidate.isContentFile()) {
					result = candidate;
					break;
				}
			}
			return result;
		}

		/**
		 * Returns the members of this group.
		 * <p>
		 * 
		 * @return the members of this group.
		 */
		public List<SiblingCandidate> getMembers() {
			return this.m_members;
		}

		/**
		 * Returns the list of members in this group.
		 * <p>
		 * 
		 * @return the list of members in this group.
		 */
		@Override
		public String toString() {
			return this.m_members.toString();
		}

		/**
		 * Validates this group of siblings (same resource uuid):
		 * <ul>
		 * <li>Resource uuid has to be the same for all candidates.</li>
		 * <li>A candidate with content file has to exist.</li>
		 * <li>No two candidates with content file have to exist.</li>
		 * <li>Structure uuid has to be different for all candidates (if it is
		 * written in the property file).</li>
		 * </ul>
		 * Also each sibling candidate is validated:
		 * <ul>
		 * <li>The type has to be configured in the meta file.</li>
		 * </ul>
		 * 
		 * <p>
		 * The actions taken are controlled by the ANT task attribute
		 * <code>{@link ManifestBuilderTask#setFailOnIllegalMetaFile(boolean)}</code>
		 * and
		 * <code>{@link ManifestBuilderTask#setDeleteIllegalMetaFiles(boolean)}</code>
		 * . If those are set to false and true various actions may be performed
		 * to "heal" a broken build environment:
		 * <ul>
		 * <li>Delete additional content files of a sibling group (the older
		 * one)</li>
		 * <li>Delete illegal meta files (missing type, missing content file in
		 * sibling group -> remainders from content file deletion with
		 * forgetting to delete meta file)</li>
		 * </ul>
		 * <p>
		 * 
		 * @return true if everything is OK, false if errors were found but
		 *         actions (deletions of meta files and also the candidates
		 *         related to it) have been taken to wipe them out or throws a
		 *         build exceptions if errors were found and no actions have
		 *         been taken.
		 */
		public boolean validate() {

			ManifestBuilderTask.this.log("Validation on sibling group with "
					+ this.getCandidateCount() + " members: " + this.m_members,
					Project.MSG_VERBOSE);
			boolean result = true;
			// Step 1: check if resource uuid is set in case of more than one
			// member in group (should never fail unless code to collect
			// groups is wrong)
			// Also check if different resource uuids are in this group (just in
			// case of wrong code)

			if (this.getCandidateCount() > 1) {
				CmsUUID resourceID = CmsUUID.getNullUUID();
				CmsUUID curResourceID = CmsUUID.getNullUUID();

				for (SiblingCandidate candidate : this.m_members) {
					curResourceID = candidate.getResourceUUID();
					if (curResourceID.isNullUUID()) {
						throw new BuildException(
								"Found meta file considered as sibling with missing resource uuid ("
										+ OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_PREFIX
										+ OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_SUFFIX_RESOURCE_UUID
										+ "): " + candidate.getMetaFilePath());
					}
					if (!resourceID.isNullUUID()) {
						if (!resourceID.equals(curResourceID)) {
							// should never happen (programming error in
							// collection of sibling groups only)
							throw new BuildException(
									"Found meta file considered as sibling with alternating resource uuid ("
											+ OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_PREFIX
											+ OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_SUFFIX_RESOURCE_UUID
											+ "): "
											+ candidate.getMetaFilePath());
						}
					}
				}

			}
			// Step 2: check for duplicate structure uuid
			CmsUUID structureID = CmsUUID.getNullUUID();
			Set<CmsUUID> uuids = new TreeSet<CmsUUID>();
			boolean failedToAdd = false;
			for (SiblingCandidate candidate : this.m_members) {
				structureID = candidate.getStructureUUID();
				if (structureID.isNullUUID()) {
					failedToAdd = false;
				} else {
					failedToAdd = !uuids.add(structureID);
				}
				if (failedToAdd) {
					throw new BuildException(
							"Duplicate structure uuid ("
									+ OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_PREFIX
									+ OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_SUFFIX_STRUCTURE_UUID
									+ ")for file "
									+ candidate.getMetaFilePath());
				}
			}
			// Step 3: check for existing content file:
			if (this.getCandidateWithContent() == null) {
				if (ManifestBuilderTask.this.isDeleteIllegalMetaFiles()) {
					// delete this file from disk:
					ManifestBuilderTask.this
							.log(
									"Deleting meta file with missing content because \"deleteIllegalMetaFiles\" is set to true. ",
									Project.MSG_INFO);
					// Cannot use enhanced for loop as remove is required!
					Iterator<SiblingCandidate> itMembers = this.m_members
							.iterator();
					SiblingCandidate candidate;
					while (itMembers.hasNext()) {
						candidate = itMembers.next();
						File delete = new File(candidate.getMetaFilePath());
						ManifestBuilderTask.this.log(
								"Deleting meta file with missing content: "
										+ delete.getAbsolutePath(),
								Project.MSG_WARN);
						boolean deleted = delete.delete();
						if (!deleted) {
							ManifestBuilderTask.this
									.log(
											"Failed to delete illegal file "
													+ delete.getAbsolutePath()
													+ " Retrying with deleteOnExit(). Deletion cannot be ensured.\n Check if the file remains and delete it manually if so. ",
											Project.MSG_WARN);
							delete.deleteOnExit();
						}
						itMembers.remove();
						// if(! deletedFromDataModel) {
						// throw new
						// BuildException("Failed to delete "+candidate.getVfsPath()+" from data model. Cannot continue in undefined state. ");
						// }
						result = false;
					}
				} else {

					throw new BuildException(
							"Meta file without any content file or sibling with content file : "
									+ this.m_members.get(0).getMetaFilePath()
									+ ". \nYou may either put in a valid content file, delete the meta file \nor  set attribute \"deleteIllegalMetaFiles\". ");
				}
			}
			// Step 4: let each candidate validate itself:
			for (SiblingCandidate candidate : this.m_members) {
				candidate.validate();
			}
			return result;
		}
	}

	/**
	 * Factored out property loading.
	 * <p>
	 * 
	 * @param metaFile
	 *            the file containing properties.
	 * 
	 * @return the loaded properties.
	 * 
	 * @throws IOException
	 *             if something bad happens.
	 * 
	 * @throws FileNotFoundException
	 *             if something bad happens.
	 */
	public static Properties loadProperties(File metaFile)
			throws FileNotFoundException, IOException {
		Properties props = new Properties();
		props.load(new FileInputStream(metaFile));
		return props;
	}

	/**
	 * Debugging hook only.
	 * <p>
	 * 
	 * @param args
	 *            ignored.
	 * 
	 * @throws IOException
	 *             if something goes wrong.
	 */
	public static void main(final String[] args) throws IOException {

		final File baseDir = new File(".");

		final ManifestBuilderTask task = new ManifestBuilderTask();
		task.setGenerateuuids(true);
		task.setDistfolder("./dist");
		task.setSrcfolder("./testmodules/com.questico.cms.frontend");
		task
				.setResourcetypes("./modules/com.questico.cms.frontend/resourcetypes.xml");
		task
				.setExplorertypes("./modules/com.questico.cms.frontend/explorertypes.xml");
		task.setOpencmsversion("7.5.1");
		task.setVersion("000");
		task.setFailOnMissingPropertyFile(false);

		final FileSet fs = new FileSet();
		final File moduleInputFolder = new File(baseDir,
				"/etc/samples/modules_src/com.eurelis.opencms.module.name/");
		fs.setDir(moduleInputFolder);
		fs
				.setExcludes("**/__properties,**/__properties/**,**/__acl,**/__acl/**");
		final Project proj = new Project();
		proj.setBaseDir(baseDir);
		fs.setProject(proj);
		task.setProject(proj);
		task.addFileset(fs);

		// capture output (as not run from ANT but programmatically):
		final Project project = new Project();
		project.setBaseDir(new File("."));
		final DefaultLogger consoleLogger = new DefaultLogger();
		consoleLogger.setErrorPrintStream(System.err);
		consoleLogger.setOutputPrintStream(System.out);
		consoleLogger.setMessageOutputLevel(Project.MSG_WARN);
		project.addBuildListener(consoleLogger);
		project.init();
		task.setProject(project);

		// finally!
		task.execute();

		// OutputFormat format = OutputFormat.createPrettyPrint();
		// XMLWriter writer = new XMLWriter(System.out, format);
		// writer.write(doc);
		// writer.close();
	}

	private final DateFormat m_dateformat;

	/**
	 * If true file types will be guessed if they cannot be read from the
	 * property file / property file missing.
	 */
	private boolean m_guessFileTypes = true;

	/**
	 * If true illegal meta files (uuid conflicts, missing content file,...) are
	 * deleted from the build environment.
	 */
	private boolean m_deleteIllegalMetaFiles = false;

	/** The folder to write the manifest.xml to. */
	private String m_distfolder;

	Document m_document;

	/** The path to the explorer type xml file. */
	private String m_explorertypes;

	/**
	 * If true illegal meta files (uuid conflicts, missing content file,...)
	 * will cause a build failure.
	 */
	private boolean m_failOnIllegalMetaFile;

	/**
	 * If true every access control file that is missing will cause a build
	 * failure.
	 */
	private boolean m_failOnMissingAclFile = false;

	/**
	 * If true every property file that is missing will cause a build failure.
	 */
	private boolean m_failOnMissingPropertyFile = true;

	/**
	 * The file sets for the module resources to incorporate in the manifest.xml
	 * document.
	 */
	private final List<FileSet> m_filesets;

	/** If true uuids will be invented if missing in the property files. */
	private boolean m_generateuuids;

	/** The path to the module info xml file. */
	private String m_moduleinfo;

	/** The name of this module. */
	private String m_name;

	private String m_opencmsversion;

	/** The path to the resource type xml file. */
	private String m_resourcetypes;

	/** The source folder. */
	private String m_srcfolder;

	private String m_version;
	/** Used for guessing OpenCms file types by extension. */
	private Map<String, String> m_fileExtension2ResourceTypes = new HashMap<String, String>();

	public ManifestBuilderTask() {
		this.m_dateformat = new SimpleDateFormat(
				"EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
		this.m_document = null;
		this.m_resourcetypes = null;
		this.m_explorertypes = null;
		this.m_generateuuids = false;
		this.m_distfolder = null;
		this.m_srcfolder = null;
		this.m_filesets = new LinkedList<FileSet>();
		this.m_opencmsversion = "";
		this.m_version = "";
		this.m_failOnMissingPropertyFile = true;
	}

	/**
	 * Adds the access control entries to the manifest.xml.
	 * <p>
	 * 
	 * @param fileEntry
	 *            the xml node for the file.
	 * 
	 * @param aclFilePath
	 *            the path to the acl file.
	 */
	private void addAccessesToTree(final Element fileEntry,
			final String aclFilePath) {
		if (null != aclFilePath) {
			final File xml = new File(aclFilePath);
			if (!xml.exists()) {
				fileEntry.addElement("accesscontrol");
				if (this.isFailOnMissingAclFile()) {
					throw new BuildException(
							"Access control file "
									+ aclFilePath
									+ " missing. \nYou can accept this by setting attribute \"failOnMissingAclFile\" to \"false\"");
				} else {
					this.log(
							"Access control file " + aclFilePath + " missing.",
							Project.MSG_ERR);
				}
			} else {
				if (xml.length() < 10) {
					this.log("Ignoring empty acl file " + aclFilePath,
							Project.MSG_WARN);
				} else {

					final SAXReader reader = new SAXReader();
					try {
						final Document doc = reader.read(xml);
						final Element elem = doc.getRootElement();
						if (null != elem) {
							fileEntry.add(elem);
						} else {
							fileEntry.addElement("accesscontrol");
						}
					} catch (final Throwable e) {
						fileEntry.addElement("accesscontrol");
						if (this.isFailOnMissingAclFile()) {
							throw new BuildException(
									"Problem reading access control file "
											+ aclFilePath
											+ ".\nYou can accept this by setting attribute \"failOnMissingAclFile\" to \"false\"",
									e);
						} else {
							this.log("Problem reading access control file "
									+ aclFilePath + ".", e, Project.MSG_ERR);
						}
					}
				}
			}
		}
	}

	/**
	 * Support for the nested &lt;fileset&gt; attributes that have to be added
	 * to specify the module files.
	 * <p>
	 * 
	 * This is intended for ANT and does not have to be invoked by custom java
	 * code.
	 * <p>
	 * 
	 * @param fileset
	 *            the file set to add.
	 */
	public void addFileset(final FileSet fileset) {
		this.m_filesets.add(fileset);
	}

	/**
	 * Adds the &lt;property &gt; nodes to the &gt;file&gt; node given by the
	 * file argument.
	 * <p>
	 * 
	 * @param file
	 *            the &lt;file&gt; node to process.
	 * 
	 * @param props
	 *            the loaded properties to add.
	 */
	private void addPropertiesToTree(final Element file, final Properties props) {
		if (!props.isEmpty()) {
			final Iterator<Object> i$ = props.keySet().iterator();
			Object keyObject;
			String name;
			String key;
			while (i$.hasNext()) {
				keyObject = i$.next();
				key = (String) keyObject;
				name = key.substring(0, key.length() - 2);
				/*
				 * Check the name just as OpenCms does to fail directly here
				 * instead failing when installing.
				 */
				CmsPropertyDefinition.checkPropertyName(name);
				if ((props.getProperty(key).length() > 0)
						&& !key
								.contains(OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_PREFIX)) {
					try {
						if (key.endsWith(".s")) {
							final Element property = file
									.addElement("property").addAttribute(
											"type", "shared");
							property.addElement("name").addText(
									key.substring(0, key.length() - 2));
							property.addElement("value").add(
									new FlyweightCDATA(props.getProperty(key)));
						} else if (key.endsWith(".i")) {
							final Element property = file
									.addElement("property");
							property.addElement("name").addText(
									key.substring(0, key.length() - 2));
							property.addElement("value").add(
									new FlyweightCDATA(props.getProperty(key)));
						} else {
							final Element property = file
									.addElement("property");
							property.addElement("name").addText(key);
							property.addElement("value").add(
									new FlyweightCDATA(props.getProperty(key)));
						}
					} catch (final Exception e) {
						e.printStackTrace();
					}
				}
			}
		}

	}

	/**
	 * Counterpart of <code>{@link #folderPropertiesPath(String)}</code>.
	 * <p>
	 * 
	 * Translates a relative property File (meta file) path back to the relative
	 * path to the content file.
	 * <p>
	 * 
	 * <pre>
	 * String s = &quot;/system/test.txt&quot;;
	 * String t = folderPropertiesPath(s); // t is &quot;/system/__properties/test.txt.properties
	 * String t = contentFilePathForPropertiesPath(t);
	 * System.out.println(s.equals(t)); // true!
	 * </pre>
	 * 
	 * @param propertyFilePath
	 *            a relative property file (meta file) path.
	 * 
	 * @return the corresponding relative content file path.
	 */
	private String contentFilePathForPropertiesPath(
			final String propertyFilePath) {
		String result = "";
		// normalize:
		String normalizedPropertyFilePath = propertyFilePath.replace(
				File.separatorChar, '/');

		if (normalizedPropertyFilePath.contains("/")) {
			// folder detection:
			int propertyfolderindex = normalizedPropertyFilePath
					.lastIndexOf("__properties/__");
			int filenameStartIndex;
			// fallback to file detection:
			if (propertyfolderindex < 0) {
				// no folder but a file
				propertyfolderindex = normalizedPropertyFilePath
						.lastIndexOf("__properties");
				filenameStartIndex = propertyfolderindex
						+ "__properties".length() + 1;
			} else {
				filenameStartIndex = propertyfolderindex
						+ "__properties/__".length();
			}
			try {
				final StringBuilder builder = new StringBuilder()
						.append(normalizedPropertyFilePath.subSequence(0,
								propertyfolderindex));

				builder.append(normalizedPropertyFilePath.subSequence(
						filenameStartIndex,
						normalizedPropertyFilePath.length()
								- ".properties".length()).toString());
				result = builder.toString();
			} catch (final Throwable f) {
				f.printStackTrace(System.err);
				System.out.println(normalizedPropertyFilePath);
			}
		} else {
			result = (new StringBuilder()).append(
					normalizedPropertyFilePath.subSequence(("__properties/__")
							.length(), normalizedPropertyFilePath.length()
							- ".properties".length())).toString();
		}

		this.log("contentFilePathForPropertiesPath("
				+ normalizedPropertyFilePath + ") : " + result,
				Project.MSG_DEBUG);
		return result;
	}

	/**
	 * Creates the manifest.xml for the module.
	 * <p>
	 * 
	 * @return the manifext.xml for the module.
	 * 
	 * @throws FileNotFoundException
	 *             if something bad happens.
	 * 
	 * @throws IOException
	 *             if something bad happens.
	 */
	public Document createDocument() throws FileNotFoundException, IOException {
		/*
		 * Lookup for sibling groups by resource type uuid. Core structure to
		 * ensure that meta property files in the build environment are valid.
		 */
		final Map<CmsUUID, SiblingGroup> siblingGroups = new HashMap<CmsUUID, SiblingGroup>();
		this.m_document = DocumentHelper.createDocument();
		this.m_document.setXMLEncoding("UTF-8");
		final Element root = this.m_document.addElement("export");

		this.inserModuleInfo(root);

		// after this, we should have a module node:
		final Element module = root.element("module");
		this.insertResourceTypes(module);
		this.insertExplorerTypes(module);
		if (!this.m_filesets.isEmpty()) {
			// all content files of all filesets
			Set<String> allFiles = new TreeSet<String>();
			// all content foldersof all filesets
			Set<String> allFolders = new TreeSet<String>();
			for (final FileSet fileset : this.m_filesets) {
				final DirectoryScanner ds = fileset.getDirectoryScanner(fileset
						.getProject());
				final String dirs[] = ds.getIncludedDirectories();

				log("Found module directories: ", Project.MSG_DEBUG);
				// collect directories
				for (String dir : dirs) {
					// normalize:
					dir = dir.replace(File.separator, "/");
					log("  Content folder:" + dir, Project.MSG_DEBUG);
					boolean ok = allFolders.add(dir);
					if (!ok) {
						this.log("Duplicate folder " + dir
								+ " found. Check your nested filesets!",
								Project.MSG_WARN);
					}
				}
				// collect files
				for (String file : ds.getIncludedFiles()) {
					// normalize:
					file = file.replace(File.separator, "/");
					boolean ok = allFiles.add(file);
					if (!ok) {
						this.log("Duplicate file " + file
								+ " found. Check your nested filesets!",
								Project.MSG_WARN);
					} else {
						log("  Content file:" + file, Project.MSG_DEBUG);
					}
				}

				// META FILE COLLECTION START
				final FileSet siblingScannerSet = new FileSet();
				final File srcDir = new File(this.m_srcfolder);
				siblingScannerSet.setDir(srcDir);
				this.log("Scanning for siblings in dir "
						+ siblingScannerSet.getDir().getAbsolutePath(),
						Project.MSG_VERBOSE);
				siblingScannerSet.setIncludes("**/__properties/**");
				siblingScannerSet.setExcludes("**manifest.xml");
				final DirectoryScanner siblingScanner = siblingScannerSet
						.getDirectoryScanner(fileset.getProject());
				final String[] siblingFileCandidates = siblingScanner
						.getIncludedFiles();
				File siblingCandidate;
				if (siblingFileCandidates.length == 0) {
					this
							.log(
									"No sibling candidates found, something with the code seems wrong!",
									Project.MSG_ERR);
				}
				for (final String siblingCandidateString : siblingFileCandidates) {
					this.log(
							"Sibling file candidate " + siblingCandidateString,
							Project.MSG_DEBUG);
					final String siblingCandidateName = siblingCandidateString
							.substring(siblingCandidateString
									.lastIndexOf(File.separatorChar) + 1);
					this.log("Sibling candidate file name "
							+ siblingCandidateName, Project.MSG_DEBUG);
					SiblingGroup group;
					siblingCandidate = new File(fileset.getDir(),
							siblingCandidateString);
					Properties metaFileProps = ManifestBuilderTask
							.loadProperties(siblingCandidate);
					CmsUUID resourceID = CmsUUID.getNullUUID();
					String resourceIDString = metaFileProps
							.getProperty(OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_PREFIX
									+ OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_SUFFIX_RESOURCE_UUID);
					this.log("Resource UUID for " + siblingCandidateString
							+ " : \"" + resourceIDString + "\"",
							Project.MSG_VERBOSE);
					if (resourceIDString != null) {
						try {
							resourceID = new CmsUUID(resourceIDString);
							group = siblingGroups.get(resourceID);
							if (group == null) {
								this.log(
										"Could not find sibling group in map for resource UUID: "
												+ resourceIDString
												+ "\nSibling groups: "
												+ siblingGroups,
										Project.MSG_VERBOSE);
								group = new SiblingGroup();
								siblingGroups.put(resourceID, group);
							} else {
								this.log("Found sibling group to add to: "
										+ group, Project.MSG_VERBOSE);
							}
						} catch (Throwable f) {
							throw new BuildException(
									"Illegal resource id ("
											+ OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_PREFIX
											+ OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_SUFFIX_RESOURCE_UUID
											+ ") in file "
											+ siblingCandidate
													.getAbsolutePath());
						}
					} else {
						this.log("Missing resource id for "
								+ siblingCandidate.getAbsolutePath(),
								Project.MSG_VERBOSE);
						// invent a new one as it is missing: This is definitely
						// no sibling but unique
						resourceID = new CmsUUID();
						group = new SiblingGroup();
						siblingGroups.put(resourceID, group);
					}
					group
							.addCandidate(
									siblingCandidate,
									metaFileProps,
									this
											.contentFilePathForPropertiesPath(siblingCandidateString));

				}
				// META FILE COLLECTION STOP
			}

			// FILESET COLLECTION STOP
			boolean validWithoutAnyUnderlyingCorrectionsTaken = true;
			for (SiblingGroup group : siblingGroups.values()) {
				validWithoutAnyUnderlyingCorrectionsTaken &= group.validate();
			}
			if (!validWithoutAnyUnderlyingCorrectionsTaken) {
				String message = "Modification were performed on the build tree due to illegal meta files.";
				if (this.isFailOnIllegalMetaFile()) {
					throw new BuildException(
							message
									+ "\nSet attribute \"failOnIllegalMetaFile\" to false if you want to allow corrections without faling build. ");
				} else {
					this.log(message, Project.MSG_WARN);
				}
			}

			/*
			 * Validation may delete some illegal sibling candidate (and their
			 * underlying files). So now we can use the entries still there.
			 * Lookup for SiblingCandidate instances (data model for meta
			 * property files) by absolute content file path. This is needed
			 * when running over all content files to get the matching
			 * properties. The instances are the same as those in the sibling
			 * groups of "siblingGroups" above.
			 */
			final Map<String, com.eurelis.opencms.ant.task.ManifestBuilderTask.SiblingGroup.SiblingCandidate> contentFilePath2SiblingCandidates = new HashMap<String, com.eurelis.opencms.ant.task.ManifestBuilderTask.SiblingGroup.SiblingCandidate>();
			for (SiblingGroup group : siblingGroups.values()) {

				for (com.eurelis.opencms.ant.task.ManifestBuilderTask.SiblingGroup.SiblingCandidate candidate : group
						.getMembers()) {
					this
							.log("Storing sibling candidate with path "
									+ candidate.getVfsPath()
									+ " for later processing.");
					contentFilePath2SiblingCandidates.put(candidate
							.getVfsPath(), candidate);
				}
			}

			// MANIFEST ENTRY CREATION START
			final Element files = root.addElement("files");
			// FOLDERS MANAGEMENT
			com.eurelis.opencms.ant.task.ManifestBuilderTask.SiblingGroup.SiblingCandidate candidate;
			for (final String filepath : allFolders) {
				if (filepath == "") {
					continue;
				}
				// normalize:
				final String filepathUnix = filepath.replace(
						File.separatorChar, '/');

				candidate = contentFilePath2SiblingCandidates
						.remove(filepathUnix);
				if (candidate == null) {
					this.log("Cannot find candidate for path " + filepathUnix
							+ ".\nMeta file pool:\n"
							+ contentFilePath2SiblingCandidates,
							Project.MSG_INFO);
					if (this.isFailOnMissingPropertyFile()) {
						throw new BuildException(
								"Missing meta file for content file "
										+ filepathUnix
										+ ". Expected: "
										+ folderPropertiesPath(filepathUnix)
										+ "\nYou can accept this by setting attribute \"failOnMissingPropertyFile\" to \"true\" (not recommended)");
					}
				}
				log("Adding folder " + filepathUnix, Project.MSG_INFO);
				final Element tmpFile = files.addElement("file");
				tmpFile.addElement("destination").addText(filepathUnix);
				String type;
				if (candidate == null) {
					type = this.guessFileType(filepathUnix);
				} else {
					type = candidate.getType();
				}
				tmpFile.addElement("type").addText(type);
				if (this.isGenerateuuids()) {
					final Element uuidNode = tmpFile
							.addElement("uuidstructure");
					CmsUUID tmpUUID = candidate.getStructureUUID();
					if (tmpUUID.isNullUUID()) {
						tmpUUID = new CmsUUID();
					}
					uuidNode.addText(tmpUUID.toString());
				}
				tmpFile.addElement("datelastmodified").addText(
						this.m_dateformat.format(Long.valueOf((new File(
								(new StringBuilder()).append(this.m_srcfolder)
										.append(File.separator)
										.append(filepath).toString()))
								.lastModified())));
				// WARNING : CONSTANT VALUE
				tmpFile.addElement("userlastmodified").addText("Admin");
				tmpFile.addElement("datecreated").addText(
						this.m_dateformat.format(Long.valueOf((new File(
								(new StringBuilder()).append(this.m_srcfolder)
										.append(File.separator)
										.append(filepath).toString()))
								.lastModified())));
				// WARNING : CONSTANT VALUE
				tmpFile.addElement("usercreated").addText("Admin");
				tmpFile.addElement("flags").addText("0");
				final Element properties = tmpFile.addElement("properties");

				// props detection and implementation
				this.addPropertiesToTree(properties, candidate
						.getPropertyFile());
				final String tmpAccessFile = (new StringBuilder()).append(
						this.m_srcfolder).append('/').append(
						this.folderAccessesPath(filepath)).toString();
				this.addAccessesToTree(tmpFile, tmpAccessFile);
			}

			// FILES MANAGEMENT
			for (final String filepath : allFiles) {
				// This is done for the manifest.xml which has / as file
				// separator by definition.
				final String filepathUnix = filepath.replace(
						File.separatorChar, '/');
				if (filepath != "") {
					log("Adding file " + filepathUnix, Project.MSG_INFO);
					final Element tmpFile = files.addElement("file");
					com.eurelis.opencms.ant.task.ManifestBuilderTask.SiblingGroup.SiblingCandidate metaFile = contentFilePath2SiblingCandidates
							.remove(filepathUnix);
					if (metaFile == null) {
						this.log("Meta file pool:\n"
								+ contentFilePath2SiblingCandidates,
								Project.MSG_INFO);
						throw new BuildException(
								"Missing meta file for content file "
										+ filepathUnix);
					}

					/*
					 * Find out the resource ID to track siblings:
					 */
					final boolean hasContent = metaFile.isContentFile();
					CmsUUID resourceUUID = metaFile.getResourceUUID();
					if (resourceUUID.isNullUUID()) {
						if (this.m_generateuuids) {
							resourceUUID = new CmsUUID();
						}
					}

					if (hasContent) {
						this.log(
								"Creation <source> node for sibling with content "
										+ filepathUnix, Project.MSG_VERBOSE);
						tmpFile.addElement("source").addText(filepathUnix);
					} else {
						this.log(
								"Skipping creation of <source> node for sibling without content "
										+ filepathUnix, Project.MSG_VERBOSE);
					}
					tmpFile.addElement("destination").addText(filepathUnix);
					String type = metaFile.getType();
					tmpFile.addElement("type").addText(type);
					if (this.isGenerateuuids()) {
						final Element uuidNode = tmpFile
								.addElement("uuidresource");
						uuidNode.addText(resourceUUID.getStringValue());

						final Element uuidNode2 = tmpFile
								.addElement("uuidstructure");
						CmsUUID structureUUID = metaFile.getStructureUUID();
						if (structureUUID.isNullUUID()) {
							structureUUID = new CmsUUID();
						}
						uuidNode2.addText(structureUUID.getStringValue());
					}
					tmpFile.addElement("datelastmodified").addText(
							this.m_dateformat.format(Long.valueOf(new File(
									metaFile.getContentFilePath())
									.lastModified())));
					tmpFile.addElement("userlastmodified").addText("Admin");
					tmpFile.addElement("datecreated").addText(
							this.m_dateformat.format(Long.valueOf(new File(
									metaFile.getContentFilePath())
									.lastModified())));
					// WARNING: Hardcoded
					tmpFile.addElement("usercreated").addText("Admin");
					tmpFile.addElement("flags").addText("0");
					final Element properties = tmpFile.addElement("properties");
					this.addPropertiesToTree(properties, metaFile
							.getPropertyFile());
					tmpFile.addElement("accesscontrol");
					final String tmpAccessFile = (new StringBuilder()).append(
							this.m_srcfolder).append('/').append(
							this.fileAccessesPath(filepath)).toString();
					this.addAccessesToTree(tmpFile, tmpAccessFile);
				}
			}
			log("Remaining siblings without content are added: "
					+ contentFilePath2SiblingCandidates.values(),
					Project.MSG_INFO);
			// ADD remaining SIBLINGS:
			// we iterated over all content files but for siblings there is no
			// content file. They remain in the map:
			for (com.eurelis.opencms.ant.task.ManifestBuilderTask.SiblingGroup.SiblingCandidate sibling : contentFilePath2SiblingCandidates
					.values()) {
				this.log("Adding sibling "
						+ this.contentFilePathForPropertiesPath(sibling
								.getMetaFilePath()), Project.MSG_INFO);
				final Element tmpFile = files.addElement("file");
				// skipping source as siblings don't have an entry for that.
				tmpFile.addElement("destination").addText(sibling.getVfsPath());
				String type = sibling.getType();
				tmpFile.addElement("type").addText(type);
				if (this.isGenerateuuids()) {
					final Element uuidNode = tmpFile.addElement("uuidresource");
					uuidNode
							.addText(sibling.getResourceUUID().getStringValue());

					final Element uuidNode2 = tmpFile
							.addElement("uuidstructure");
					CmsUUID structureUUID = sibling.getStructureUUID();
					if (structureUUID.isNullUUID()) {
						structureUUID = new CmsUUID();
					}
					uuidNode2.addText(structureUUID.getStringValue());
				}
				tmpFile.addElement("datelastmodified").addText(
						this.m_dateformat.format(Long.valueOf(new File(sibling
								.getMetaFilePath()).lastModified())));
				tmpFile.addElement("userlastmodified").addText("Admin");
				tmpFile.addElement("datecreated").addText(
						this.m_dateformat.format(Long.valueOf(new File(sibling
								.getMetaFilePath()).lastModified())));
				// WARNING: Hardcoded
				tmpFile.addElement("usercreated").addText("Admin");
				tmpFile.addElement("flags").addText("0");
				final Element properties = tmpFile.addElement("properties");
				this.addPropertiesToTree(properties, sibling.getPropertyFile());
				tmpFile.addElement("accesscontrol");
				final String tmpAccessFile = (new StringBuilder()).append(
						this.m_srcfolder).append('/').append(
						this.fileAccessesPath(sibling.getVfsPath())).toString();
				this.addAccessesToTree(tmpFile, tmpAccessFile);
				this.addAccessesToTree(tmpFile, tmpAccessFile);
			}
			// MANIFEST ENTRY CREATION STOP
		}
		return this.m_document;
	}

	/**
	 * Executes this ANT task.
	 * <p>
	 */
	@Override
	public void execute() throws BuildException {
		super.execute();
		try {
			if (this.isGuessFileTypes()) {

				try {
					this.loadFileTypes();

				} catch (DocumentException e) {
					this
							.log(
									"Disabled file type guessing: Unable to load resource type mappings from opencms-vfs.xml in jar file. \n"
											+ StringUtils.getStackTrace(e),
									Project.MSG_WARN);
					this.setGuessFileTypes(false);
				}
			}
			this.createDocument();
			this.write();
		} catch (final IOException e) {
			throw new BuildException("Fatal error", e);
		}
	}

	/**
	 * Calculates the access control file path ("../__acl/file.html.xml" for a
	 * file) for a given content file.
	 * <p>
	 * 
	 * @param contentFilePath
	 *            the path to the content file, folder separator chars may be
	 *            windows or unix.
	 * 
	 * @return the access control file path for a given content file with '/' as
	 *         folder separator.
	 */
	private String fileAccessesPath(final String contentFilePath) {
		log("fileAccessesPath, argument: " + contentFilePath, Project.MSG_DEBUG);
		String normalizedContentFilePath = contentFilePath.replace(
				File.separatorChar, '/');
		String result;
		if (normalizedContentFilePath.contains("/")) {
			result = (new StringBuilder()).append(
					normalizedContentFilePath.subSequence(0,
							normalizedContentFilePath.lastIndexOf('/') + 1)
							.toString()).append("__acl/").append(
					normalizedContentFilePath.subSequence(
							normalizedContentFilePath.lastIndexOf('/') + 1,
							normalizedContentFilePath.length()).toString())
					.append(".xml").toString();
		} else {
			result = (new StringBuilder()).append("__acl/__").append(
					normalizedContentFilePath).append(".xml").toString();
		}
		return result;
	}

	/**
	 * Calculates the access control folder path ("../__acl/__folder.xml" for a
	 * folder) for a given content file.
	 * <p>
	 * 
	 * @param contentFilePath
	 *            the path to the content folder, folder separator chars may be
	 *            windows or unix.
	 * 
	 * @return the access control file path for a given content file with '/' as
	 *         folder separator.
	 */
	private String folderAccessesPath(final String contentFilePath) {
		String normalizedContentFilePath = contentFilePath.replace(
				File.separatorChar, '/');
		String result;
		if (normalizedContentFilePath.contains("/")) {
			result = (new StringBuilder()).append(
					normalizedContentFilePath.subSequence(0,
							normalizedContentFilePath.lastIndexOf('/') + 1)
							.toString()).append("__acl/__").append(
					normalizedContentFilePath.subSequence(
							normalizedContentFilePath.lastIndexOf('/') + 1,
							normalizedContentFilePath.length()).toString())
					.append(".xml").toString();
		} else {
			result = (new StringBuilder()).append("__acl/__").append(
					normalizedContentFilePath).append(".xml").toString();
		}
		return result;
	}

	/**
	 * Helper that creates the relative path to the property file (meta file) of
	 * the given relative folder path.
	 * <p>
	 * 
	 * <pre>
	 * /system/modules -&gt; /system/__properties/__module.properties // folder
	 * /system/modules/ -&gt; /system/__properties/__.properties // folder gone wrong (tailing backslash is bad)
	 * </pre>
	 * 
	 * 
	 * @param contentFolder
	 *            relative folder path, separator char for unix and windows
	 *            supported.
	 * 
	 * @return the relative path to the property file (meta file) of the given
	 *         relative path with '/' used as folder separator.
	 */
	private String folderPropertiesPath(final String contentFolder) {
		String result;
		String normalizedContentFolder = contentFolder.replace(
				File.separatorChar, '/');
		if (normalizedContentFolder.contains("/")) {
			result = (new StringBuilder()).append(
					normalizedContentFolder.subSequence(0,
							normalizedContentFolder.lastIndexOf('/') + 1)
							.toString()).append("__properties/__").append(
					normalizedContentFolder.subSequence(
							normalizedContentFolder.lastIndexOf('/') + 1,
							normalizedContentFolder.length()).toString())
					.append(".properties").toString();
		} else {
			result = (new StringBuilder()).append("__properties/__").append(
					normalizedContentFolder).append(".properties").toString();
		}
		this.log("folderPropertiesPath(" + normalizedContentFolder + "): ",
				Project.MSG_DEBUG);
		this.log("  " + result, Project.MSG_DEBUG);

		return result;

	}

	/**
	 * Guesses the file type by extraction of the extension of the given path
	 * (of a content file, vfs path or file system path is both OK) and matching
	 * it with the configuration in the version of opencms-vfs.xml in this jar
	 * file.
	 * <p>
	 * 
	 * @param filePath
	 *            the content file path ( vfs path or file system path is both
	 *            OK) to get the extension from.
	 * 
	 * @return the guessed file type.
	 */
	private String guessFileType(final String filePath) {
		String extension = CmsFileUtil.getExtension(filePath);
		String type = ManifestBuilderTask.this.m_fileExtension2ResourceTypes
				.get(extension);
		// Better throw here as only when validation is performed (there it is
		// unkown that a lookup failed):
		if (CmsStringUtil.isEmptyOrWhitespaceOnly(type)) {
			throw new BuildException(
					"Cannot determine type of file "
							+ filePath
							+ " \n (ending: \""
							+ extension
							+ "\")Please define it it in the property file.\nKnown file types for ending are: "
							+ ManifestBuilderTask.this.m_fileExtension2ResourceTypes);
		} else {
			ManifestBuilderTask.this
					.log(
							"Guessed file type \""
									+ type
									+ "\" for "
									+ filePath
									+ " as is was not written in the corresponding property file as property "
									+ OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_PREFIX
									+ OpenCmsModuleDeflaterTask.MAGIC_FILEPROPERTY_SUFFIX_TYPE,
							Project.MSG_WARN);

		}
		return type;
	}

	/**
	 * Inserts the module info xml file to the export node of the manifest.xml
	 * document to create.
	 * <p>
	 * 
	 * Some info nodes are updated with informations related to the build.
	 * <p>
	 * 
	 * @param exportNode
	 *            the &lt;export&gt; node of the manifest.xml document.
	 */
	private void inserModuleInfo(final Element exportNode) {
		if (null != this.m_explorertypes) {
			if (this.m_moduleinfo == null
					|| this.m_moduleinfo.trim().equals("")) {
				throw new BuildException(
						"Missing moduleinfo attribute. It has to point to an xml file with a <moduleinfo> top level node that contains the OpenCms standard nodes <info> and <module> node. ");
			}
			final File xml = new File(this.m_moduleinfo);
			if ((!xml.exists()) || (xml.length() == 0)) {
				throw new BuildException(
						"Invalid or no moduleinfo file defined by this module: ("
								+ this.m_moduleinfo
								+ ").\nThe moduleinfo attribute has to point to an xml file with a <moduleinfo> top level node that contains the OpenCms standard nodes <info> and <module> node. ");

			} else {

				final SAXReader reader = new SAXReader();
				try {
					final Document doc = reader.read(xml);
					final Element root = doc.getRootElement();
					Element info = root.element("info");
					info.detach();
					exportNode.add(info);
					Element module = root.element("module");
					module.detach();
					exportNode.add(module);

					// post processing follows here:
					Date date = new Date();
					Element dateCreated = module.element("datecreated");
					dateCreated.setText(this.m_dateformat.format(date));
					Element userInstalled = module.element("userinstalled");
					userInstalled.setText((String) System.getProperties().get(
							"user.name"));
					Element dateInstalled = module.element("dateinstalled");
					dateInstalled.setText(this.m_dateformat.format(date));
					Element version = module.element("version");
					version.setText(this.m_version);

					Element opencmsVersion = info.element("opencms_version");
					opencmsVersion.setText(this.m_opencmsversion);

					Element name = module.element("name");
					if (this.m_name != null && !this.m_name.trim().equals("")) {
						name.setText(this.m_name);
					}

				} catch (final DocumentException e) {
					exportNode.addElement("info");
					exportNode.addElement("module");
					throw new BuildException("Failed to insert explorertypes",
							e);
				}
			}
		}
	}

	/**
	 * Inserts the explorer type node into the manifest.xml.
	 * <p>
	 * 
	 * @param module
	 *            the module node to insert explorer types to.
	 */
	private void insertExplorerTypes(final Element module) {
		if (null != this.m_explorertypes) {
			final File xml = new File(this.m_explorertypes);
			if ((!xml.exists()) || (xml.length() == 0)) {
				log(
						"No <explorertypes> file defined by this module: Skipping.",
						Project.MSG_INFO);
			} else {

				final SAXReader reader = new SAXReader();
				try {
					final Document doc = reader.read(xml);
					final Element root = doc.getRootElement();
					module.add(root);
				} catch (final DocumentException e) {
					module.addElement("explorertypes");
					throw new BuildException("Failed to insert explorertypes",
							e);
				}
			}
		}
	}

	/**
	 * Inserts the resource type node into the manifest.xml.
	 * <p>
	 * 
	 * @param module
	 *            the module node to insert resource types to.
	 */
	private void insertResourceTypes(final Element module) {
		if (null != this.m_resourcetypes) {
			final File xml = new File(this.m_resourcetypes);
			if ((!xml.exists()) || (xml.length() == 0)) {
				log("No <resourcetypes> defined by this module: Skipping.",
						Project.MSG_INFO);
			} else {
				final SAXReader reader = new SAXReader();
				try {
					final Document doc = reader.read(xml);
					final Element root = doc.getRootElement();
					module.add(root);
				} catch (final DocumentException e) {
					module.addElement("resourcetypes");
					throw new BuildException("Failed to insert explorertypes",
							e);
				}
			}
		}
	}

	public boolean isDeleteIllegalMetaFiles() {
		return this.m_deleteIllegalMetaFiles;
	}

	/**
	 * Counterpart of <code>{@link #getMetaFileOfContentFile(File)}</code>.
	 * Returns the content file for the given meta file.
	 * <p>
	 * 
	 * @param metaFile
	 *            the meta file to find the content file for.
	 *            <p>
	 * 
	 * @return the content file for the given meta file.
	 * 
	 */
	// private File getContentFileForMetaFile(final File metaFile) {
	// File result = null;
	// final String metaFilePath = metaFile.getAbsolutePath();
	// final String contentFilePath = (new
	// StringBuilder()).append(this.contentFilePathForPropertiesPath(metaFilePath)).toString();
	// result = new File(contentFilePath);
	// return result;
	// }

	public boolean isFailOnIllegalMetaFile() {
		return this.m_failOnIllegalMetaFile;
	}

	public boolean isFailOnMissingAclFile() {
		return this.m_failOnMissingAclFile;
	}

	public boolean isFailOnMissingPropertyFile() {
		return this.m_failOnMissingPropertyFile;
	}

	protected boolean isGenerateuuids() {
		return this.m_generateuuids;
	}

	/**
	 * ANT getter: Returns true if file types will be guessed if they cannot be
	 * read from the property file / property file missing.
	 * <p>
	 * 
	 * 
	 * @return true if file types will be guessed if they cannot be read from
	 *         the property file / property file missing.
	 */
	public boolean isGuessFileTypes() {
		return this.m_guessFileTypes;
	}

	/**
	 * Loads the file types.
	 * <p>
	 * 
	 * @throws DocumentException
	 *             if something goes wrong.
	 */
	@SuppressWarnings("unchecked")
	private void loadFileTypes() throws DocumentException {
		SAXReader xmlReader = new SAXReader();
		Document opencmsvfsxml = xmlReader.read(this.getClass()
				.getResourceAsStream("opencms-vfs.xml"));
		Element elemResourcetypes = opencmsvfsxml.getRootElement();
		elemResourcetypes = elemResourcetypes.element("vfs");
		elemResourcetypes = elemResourcetypes.element("resources");
		elemResourcetypes = elemResourcetypes.element("resourcetypes");
		Iterator<Element> itTypes = elemResourcetypes.elements().iterator();
		Element type;
		Iterator<Element> itMappings;
		Element mapping;
		String extension;
		String restype;
		while (itTypes.hasNext()) {
			type = itTypes.next();
			this.log("Processing type: " + type.asXML(), Project.MSG_DEBUG);
			restype = type.attributeValue("name");
			Element mappings = type.element("mappings");
			if (mappings != null) {
				itMappings = mappings.elements("mapping").iterator();
				while (itMappings.hasNext()) {
					mapping = itMappings.next();
					this.log("Processing mapping of type(" + restype + "): "
							+ mapping.asXML(), Project.MSG_DEBUG);
					extension = mapping.attributeValue("suffix");
					this.log("Adding mapping: " + extension + " -> " + restype,
							Project.MSG_VERBOSE);
					this.m_fileExtension2ResourceTypes.put(extension, restype);
				}
			} else {
				this.log("No mappings for type " + restype, Project.MSG_DEBUG);
			}
		}
	}

	/**
	 * Tries to create the given file if it does not exist yet.
	 * <p>
	 * 
	 * @param file
	 *            the file to potentially create.
	 */
	private void potentialCreateFile(final File file) {
		if (!file.exists()) {
			try {
				boolean success = false;
				final File parentFile = new File(file.getParent());
				this.potentialCreateFolder(parentFile);
				success = file.createNewFile();
				if (success) {
					this.log("Created non existing file "
							+ file.getAbsolutePath(), Project.MSG_INFO);
				} else {
					throw new BuildException("Cannot create file "
							+ file.getAbsolutePath());
				}

			} catch (final IOException e) {
				throw new BuildException("Creation of non - existing file "
						+ file.getAbsolutePath() + " failed. ", e);
			}
		} else {
			this.log("File " + file.getAbsolutePath()
					+ " is not created as it already exists. ",
					Project.MSG_INFO);
		}

	}

	/**
	 * Tries to create the given folder if it does not exist yet.
	 * <p>
	 * 
	 * @param folder
	 *            the folder to potentially create.
	 */
	private void potentialCreateFolder(final File folder) {
		if (!folder.exists()) {
			final boolean created = folder.mkdirs();
			if (created) {
				this.log("Created non existing folder "
						+ folder.getAbsolutePath(), Project.MSG_INFO);
			} else {
				throw new BuildException("Cannot create folder "
						+ folder.getAbsolutePath());
			}

		}
	}

	/**
	 * ANT setter for attribute: If true illegal meta files (uuid conflicts,
	 * missing content file,...) are deleted from the build environment.
	 * 
	 * @param deleteIllegalMetaFiles
	 *            if true illegal meta files (uuid conflicts, missing content
	 *            file,...) are deleted from the build environment.
	 */
	public void setDeleteIllegalMetaFiles(boolean deleteIllegalMetaFiles) {
		this.m_deleteIllegalMetaFiles = deleteIllegalMetaFiles;
	}

	/**
	 * Sets the folder to write the manifest xml to.
	 * <p>
	 * 
	 * @param distFolder
	 *            the folder to write the manifest xml to.
	 */
	public void setDistfolder(final String distFolder) {
		this.m_distfolder = distFolder;
	}

	/**
	 * Sets the path to the exporertypes xml file to integrate in the manifest
	 * xml.
	 * <p>
	 * 
	 * @param explorerTypes
	 *            the path to the exporertypes xml file to integrate in the
	 *            manifest xml.
	 */
	public void setExplorertypes(final String explorerTypes) {
		this.m_explorertypes = explorerTypes;
	}

	/**
	 * ANT setter for attribute: If true build will fail if a property file
	 * corresponding to a content file is illegal (same structure uuid for two
	 * siblings, missing content file,...).
	 * <p>
	 * 
	 * @param failOnIllegalMetaFile
	 *            If true build will fail if a property corresponding to a
	 *            content file is missing.
	 */
	public void setFailOnIllegalMetaFile(boolean failOnIllegalMetaFile) {
		this.m_failOnIllegalMetaFile = failOnIllegalMetaFile;
	}

	/**
	 * ANT setter for attribute: If true a missing access control xml file will
	 * cause the build to fail.
	 * <p>
	 * 
	 * @param failOnMissingAclFile
	 *            if true a missing access control xml file will cause the build
	 *            to fail.
	 */
	public void setFailOnMissingAclFile(boolean failOnMissingAclFile) {
		this.m_failOnMissingAclFile = failOnMissingAclFile;
	}

	/**
	 * ANT setter for attribute: If true build will fail if a property file
	 * corresponding to a content file is missing.
	 * <p>
	 * 
	 * @param failOnMissingPropertyFile
	 *            If true build will fail if a property corresponding to a
	 *            content file is missing.
	 */
	public void setFailOnMissingPropertyFile(boolean failOnMissingPropertyFile) {
		this.m_failOnMissingPropertyFile = failOnMissingPropertyFile;
	}

	/**
	 * ANT support mode for the generateuuids attribute.
	 * <p>
	 * 
	 * @param generateuuids
	 *            if "true" or "on" UUIDs will be invented if missing in the
	 *            property files.
	 */
	public void setGenerateuuids(final boolean generateuuids) {
		this.m_generateuuids = generateuuids;
	}

	/**
	 * ANT Setter: Sets if file types will be guessed if they cannot be read
	 * from the property file / property file missing.
	 * 
	 * @param guessFileTypes
	 *            true if file types will be guessed if they cannot be read from
	 *            the property file / property file missing.
	 */
	public void setGuessFileTypes(boolean guessFileTypes) {
		this.m_guessFileTypes = guessFileTypes;
	}

	/**
	 * Sets the path to the module info xml file.
	 * <p>
	 * 
	 * @param moduleinfo
	 *            the path to the module info xml file.
	 */
	public void setModuleinfo(String moduleinfo) {
		this.m_moduleinfo = moduleinfo;
	}

	/**
	 * ANT setter: Sets the name of the module.
	 * <p>
	 * 
	 * @param name
	 *            the name of the module.
	 */
	public void setName(final String name) {
		this.m_name = name;
	}

	/**
	 * ANT setter: Sets the OpenCms version.
	 * <p>
	 * This is used in the module info node and will be displayed in the OpenCms
	 * workplace module manager.
	 * <p>
	 * 
	 * @param opencmsversion
	 *            the OpenCms version
	 */
	public void setOpencmsversion(final String opencmsversion) {
		this.m_opencmsversion = opencmsversion;
	}

	/**
	 * ANT setter: Sets the path to the resource types xml file to integrate in
	 * the manifest xml file for the module.
	 * <p>
	 * 
	 * @param resourceTypes
	 *            the path to the resource types xml file to integrate in the
	 *            manifest xml file for the module.
	 */
	public void setResourcetypes(final String resourceTypes) {
		this.m_resourcetypes = resourceTypes;
	}

	/**
	 * ANT setter for the attribute.
	 * <p>
	 * 
	 * Normalizes the path to use '/' as the separator char.
	 * <p>
	 * 
	 * @param srcFolder
	 *            the source folder where the content of the module is located.
	 */
	public void setSrcfolder(final String srcFolder) {
		this.m_srcfolder = srcFolder.replace(File.separatorChar, '/');
	}

	/**
	 * ANT setter for the module version.
	 * <p>
	 * 
	 * It is used for the manifest xml module version number (will be displayed
	 * in the OpenCms workplace module manager) as well as for the module zip
	 * file name.
	 * <p>
	 * 
	 * @param version
	 *            the module version.
	 */
	public void setVersion(final String version) {
		this.m_version = version;
	}

	/**
	 * Writes out the manifest xml file that has been generated.
	 * <p>
	 * 
	 * @throws IOException
	 *             if something bad happens.
	 */
	public void write() throws IOException {
		final File tmpFile = new File((new StringBuilder()).append(
				this.m_distfolder).append("/manifest.xml").toString());
		this.potentialCreateFile(tmpFile);
		log("Writing module meta file " + tmpFile.getAbsolutePath(),
				Project.MSG_INFO);
		final FileOutputStream fos = new FileOutputStream(tmpFile);
		final OutputFormat format = OutputFormat.createPrettyPrint();
		final XMLWriter writer = new XMLWriter(fos, format);
		writer.write(this.m_document);
		writer.close();
	}
}